{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2024-07-19T02:41:02.921465200Z",
     "start_time": "2024-07-19T02:40:53.602794600Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "sys.version_info(major=3, minor=12, micro=3, releaselevel='final', serial=0)\n",
      "matplotlib 3.9.0\n",
      "numpy 1.26.4\n",
      "pandas 2.2.2\n",
      "sklearn 1.5.0\n",
      "torch 2.3.1+cpu\n",
      "cpu\n"
     ]
    }
   ],
   "source": [
    "import matplotlib as mpl\n",
    "import matplotlib.pyplot as plt\n",
    "%matplotlib inline\n",
    "import numpy as np\n",
    "import sklearn\n",
    "import pandas as pd\n",
    "import os\n",
    "import sys\n",
    "import time\n",
    "from tqdm.auto import tqdm\n",
    "import torch\n",
    "import torch.nn as nn\n",
    "import torch.nn.functional as F\n",
    "\n",
    "print(sys.version_info)\n",
    "for module in mpl, np, pd, sklearn, torch:\n",
    "    print(module.__name__, module.__version__)\n",
    "    \n",
    "device = torch.device(\"cuda:0\") if torch.cuda.is_available() else torch.device(\"cpu\")\n",
    "print(device)\n"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 准备数据"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2024-07-19T02:41:05.533855700Z",
     "start_time": "2024-07-19T02:41:05.176902600Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      ".. _california_housing_dataset:\n",
      "\n",
      "California Housing dataset\n",
      "--------------------------\n",
      "\n",
      "**Data Set Characteristics:**\n",
      "\n",
      ":Number of Instances: 20640\n",
      "\n",
      ":Number of Attributes: 8 numeric, predictive attributes and the target\n",
      "\n",
      ":Attribute Information:\n",
      "    - MedInc        median income in block group\n",
      "    - HouseAge      median house age in block group\n",
      "    - AveRooms      average number of rooms per household\n",
      "    - AveBedrms     average number of bedrooms per household\n",
      "    - Population    block group population\n",
      "    - AveOccup      average number of household members\n",
      "    - Latitude      block group latitude\n",
      "    - Longitude     block group longitude\n",
      "\n",
      ":Missing Attribute Values: None\n",
      "\n",
      "This dataset was obtained from the StatLib repository.\n",
      "https://www.dcc.fc.up.pt/~ltorgo/Regression/cal_housing.html\n",
      "\n",
      "The target variable is the median house value for California districts,\n",
      "expressed in hundreds of thousands of dollars ($100,000).\n",
      "\n",
      "This dataset was derived from the 1990 U.S. census, using one row per census\n",
      "block group. A block group is the smallest geographical unit for which the U.S.\n",
      "Census Bureau publishes sample data (a block group typically has a population\n",
      "of 600 to 3,000 people).\n",
      "\n",
      "A household is a group of people residing within a home. Since the average\n",
      "number of rooms and bedrooms in this dataset are provided per household, these\n",
      "columns may take surprisingly large values for block groups with few households\n",
      "and many empty houses, such as vacation resorts.\n",
      "\n",
      "It can be downloaded/loaded using the\n",
      ":func:`sklearn.datasets.fetch_california_housing` function.\n",
      "\n",
      ".. topic:: References\n",
      "\n",
      "    - Pace, R. Kelley and Ronald Barry, Sparse Spatial Autoregressions,\n",
      "      Statistics and Probability Letters, 33 (1997) 291-297\n",
      "\n",
      "(20640, 8)\n",
      "(20640,)\n"
     ]
    }
   ],
   "source": [
    "from sklearn.datasets import fetch_california_housing\n",
    "\n",
    "housing = fetch_california_housing(data_home='data')\n",
    "print(housing.DESCR)\n",
    "print(housing.data.shape)\n",
    "print(housing.target.shape)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2024-04-25T02:46:58.781988800Z",
     "start_time": "2024-04-25T02:46:58.773990600Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "array([[ 8.32520000e+00,  4.10000000e+01,  6.98412698e+00,\n",
      "         1.02380952e+00,  3.22000000e+02,  2.55555556e+00,\n",
      "         3.78800000e+01, -1.22230000e+02],\n",
      "       [ 8.30140000e+00,  2.10000000e+01,  6.23813708e+00,\n",
      "         9.71880492e-01,  2.40100000e+03,  2.10984183e+00,\n",
      "         3.78600000e+01, -1.22220000e+02],\n",
      "       [ 7.25740000e+00,  5.20000000e+01,  8.28813559e+00,\n",
      "         1.07344633e+00,  4.96000000e+02,  2.80225989e+00,\n",
      "         3.78500000e+01, -1.22240000e+02],\n",
      "       [ 5.64310000e+00,  5.20000000e+01,  5.81735160e+00,\n",
      "         1.07305936e+00,  5.58000000e+02,  2.54794521e+00,\n",
      "         3.78500000e+01, -1.22250000e+02],\n",
      "       [ 3.84620000e+00,  5.20000000e+01,  6.28185328e+00,\n",
      "         1.08108108e+00,  5.65000000e+02,  2.18146718e+00,\n",
      "         3.78500000e+01, -1.22250000e+02]])\n",
      "--------------------------------------------------\n",
      "array([4.526, 3.585, 3.521, 3.413, 3.422])\n"
     ]
    }
   ],
   "source": [
    "# print(housing.data[0:5])\n",
    "import pprint  #打印的格式比较 好看\n",
    "\n",
    "pprint.pprint(housing.data[0:5])\n",
    "print('-'*50)\n",
    "pprint.pprint(housing.target[0:5])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2024-07-19T02:41:10.408309200Z",
     "start_time": "2024-07-19T02:41:10.010403Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "(11610, 8) (11610,)\n",
      "(3870, 8) (3870,)\n",
      "(5160, 8) (5160,)\n"
     ]
    }
   ],
   "source": [
    "from sklearn.model_selection import train_test_split\n",
    "\n",
    "#拆分训练集和测试集，random_state是随机种子,同样的随机数种子，是为了得到同样的随机值\n",
    "x_train_all, x_test, y_train_all, y_test = train_test_split(\n",
    "    housing.data, housing.target, random_state = 7)\n",
    "x_train, x_valid, y_train, y_valid = train_test_split(\n",
    "    x_train_all, y_train_all, random_state = 11)\n",
    "# 训练集\n",
    "print(x_train.shape, y_train.shape)\n",
    "# 验证集\n",
    "print(x_valid.shape, y_valid.shape)\n",
    "# 测试集\n",
    "print(x_test.shape, y_test.shape)\n",
    "\n",
    "dataset_maps = {\n",
    "    \"train\": [x_train, y_train],\n",
    "    \"valid\": [x_valid, y_valid],\n",
    "    \"test\": [x_test, y_test],\n",
    "}\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2024-07-19T02:41:13.987854700Z",
     "start_time": "2024-07-19T02:41:13.971290700Z"
    }
   },
   "outputs": [
    {
     "data": {
      "text/plain": "StandardScaler()",
      "text/html": "<style>#sk-container-id-1 {\n  /* Definition of color scheme common for light and dark mode */\n  --sklearn-color-text: black;\n  --sklearn-color-line: gray;\n  /* Definition of color scheme for unfitted estimators */\n  --sklearn-color-unfitted-level-0: #fff5e6;\n  --sklearn-color-unfitted-level-1: #f6e4d2;\n  --sklearn-color-unfitted-level-2: #ffe0b3;\n  --sklearn-color-unfitted-level-3: chocolate;\n  /* Definition of color scheme for fitted estimators */\n  --sklearn-color-fitted-level-0: #f0f8ff;\n  --sklearn-color-fitted-level-1: #d4ebff;\n  --sklearn-color-fitted-level-2: #b3dbfd;\n  --sklearn-color-fitted-level-3: cornflowerblue;\n\n  /* Specific color for light theme */\n  --sklearn-color-text-on-default-background: var(--sg-text-color, var(--theme-code-foreground, var(--jp-content-font-color1, black)));\n  --sklearn-color-background: var(--sg-background-color, var(--theme-background, var(--jp-layout-color0, white)));\n  --sklearn-color-border-box: var(--sg-text-color, var(--theme-code-foreground, var(--jp-content-font-color1, black)));\n  --sklearn-color-icon: #696969;\n\n  @media (prefers-color-scheme: dark) {\n    /* Redefinition of color scheme for dark theme */\n    --sklearn-color-text-on-default-background: var(--sg-text-color, var(--theme-code-foreground, var(--jp-content-font-color1, white)));\n    --sklearn-color-background: var(--sg-background-color, var(--theme-background, var(--jp-layout-color0, #111)));\n    --sklearn-color-border-box: var(--sg-text-color, var(--theme-code-foreground, var(--jp-content-font-color1, white)));\n    --sklearn-color-icon: #878787;\n  }\n}\n\n#sk-container-id-1 {\n  color: var(--sklearn-color-text);\n}\n\n#sk-container-id-1 pre {\n  padding: 0;\n}\n\n#sk-container-id-1 input.sk-hidden--visually {\n  border: 0;\n  clip: rect(1px 1px 1px 1px);\n  clip: rect(1px, 1px, 1px, 1px);\n  height: 1px;\n  margin: -1px;\n  overflow: hidden;\n  padding: 0;\n  position: absolute;\n  width: 1px;\n}\n\n#sk-container-id-1 div.sk-dashed-wrapped {\n  border: 1px dashed var(--sklearn-color-line);\n  margin: 0 0.4em 0.5em 0.4em;\n  box-sizing: border-box;\n  padding-bottom: 0.4em;\n  background-color: var(--sklearn-color-background);\n}\n\n#sk-container-id-1 div.sk-container {\n  /* jupyter's `normalize.less` sets `[hidden] { display: none; }`\n     but bootstrap.min.css set `[hidden] { display: none !important; }`\n     so we also need the `!important` here to be able to override the\n     default hidden behavior on the sphinx rendered scikit-learn.org.\n     See: https://github.com/scikit-learn/scikit-learn/issues/21755 */\n  display: inline-block !important;\n  position: relative;\n}\n\n#sk-container-id-1 div.sk-text-repr-fallback {\n  display: none;\n}\n\ndiv.sk-parallel-item,\ndiv.sk-serial,\ndiv.sk-item {\n  /* draw centered vertical line to link estimators */\n  background-image: linear-gradient(var(--sklearn-color-text-on-default-background), var(--sklearn-color-text-on-default-background));\n  background-size: 2px 100%;\n  background-repeat: no-repeat;\n  background-position: center center;\n}\n\n/* Parallel-specific style estimator block */\n\n#sk-container-id-1 div.sk-parallel-item::after {\n  content: \"\";\n  width: 100%;\n  border-bottom: 2px solid var(--sklearn-color-text-on-default-background);\n  flex-grow: 1;\n}\n\n#sk-container-id-1 div.sk-parallel {\n  display: flex;\n  align-items: stretch;\n  justify-content: center;\n  background-color: var(--sklearn-color-background);\n  position: relative;\n}\n\n#sk-container-id-1 div.sk-parallel-item {\n  display: flex;\n  flex-direction: column;\n}\n\n#sk-container-id-1 div.sk-parallel-item:first-child::after {\n  align-self: flex-end;\n  width: 50%;\n}\n\n#sk-container-id-1 div.sk-parallel-item:last-child::after {\n  align-self: flex-start;\n  width: 50%;\n}\n\n#sk-container-id-1 div.sk-parallel-item:only-child::after {\n  width: 0;\n}\n\n/* Serial-specific style estimator block */\n\n#sk-container-id-1 div.sk-serial {\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  background-color: var(--sklearn-color-background);\n  padding-right: 1em;\n  padding-left: 1em;\n}\n\n\n/* Toggleable style: style used for estimator/Pipeline/ColumnTransformer box that is\nclickable and can be expanded/collapsed.\n- Pipeline and ColumnTransformer use this feature and define the default style\n- Estimators will overwrite some part of the style using the `sk-estimator` class\n*/\n\n/* Pipeline and ColumnTransformer style (default) */\n\n#sk-container-id-1 div.sk-toggleable {\n  /* Default theme specific background. It is overwritten whether we have a\n  specific estimator or a Pipeline/ColumnTransformer */\n  background-color: var(--sklearn-color-background);\n}\n\n/* Toggleable label */\n#sk-container-id-1 label.sk-toggleable__label {\n  cursor: pointer;\n  display: block;\n  width: 100%;\n  margin-bottom: 0;\n  padding: 0.5em;\n  box-sizing: border-box;\n  text-align: center;\n}\n\n#sk-container-id-1 label.sk-toggleable__label-arrow:before {\n  /* Arrow on the left of the label */\n  content: \"▸\";\n  float: left;\n  margin-right: 0.25em;\n  color: var(--sklearn-color-icon);\n}\n\n#sk-container-id-1 label.sk-toggleable__label-arrow:hover:before {\n  color: var(--sklearn-color-text);\n}\n\n/* Toggleable content - dropdown */\n\n#sk-container-id-1 div.sk-toggleable__content {\n  max-height: 0;\n  max-width: 0;\n  overflow: hidden;\n  text-align: left;\n  /* unfitted */\n  background-color: var(--sklearn-color-unfitted-level-0);\n}\n\n#sk-container-id-1 div.sk-toggleable__content.fitted {\n  /* fitted */\n  background-color: var(--sklearn-color-fitted-level-0);\n}\n\n#sk-container-id-1 div.sk-toggleable__content pre {\n  margin: 0.2em;\n  border-radius: 0.25em;\n  color: var(--sklearn-color-text);\n  /* unfitted */\n  background-color: var(--sklearn-color-unfitted-level-0);\n}\n\n#sk-container-id-1 div.sk-toggleable__content.fitted pre {\n  /* unfitted */\n  background-color: var(--sklearn-color-fitted-level-0);\n}\n\n#sk-container-id-1 input.sk-toggleable__control:checked~div.sk-toggleable__content {\n  /* Expand drop-down */\n  max-height: 200px;\n  max-width: 100%;\n  overflow: auto;\n}\n\n#sk-container-id-1 input.sk-toggleable__control:checked~label.sk-toggleable__label-arrow:before {\n  content: \"▾\";\n}\n\n/* Pipeline/ColumnTransformer-specific style */\n\n#sk-container-id-1 div.sk-label input.sk-toggleable__control:checked~label.sk-toggleable__label {\n  color: var(--sklearn-color-text);\n  background-color: var(--sklearn-color-unfitted-level-2);\n}\n\n#sk-container-id-1 div.sk-label.fitted input.sk-toggleable__control:checked~label.sk-toggleable__label {\n  background-color: var(--sklearn-color-fitted-level-2);\n}\n\n/* Estimator-specific style */\n\n/* Colorize estimator box */\n#sk-container-id-1 div.sk-estimator input.sk-toggleable__control:checked~label.sk-toggleable__label {\n  /* unfitted */\n  background-color: var(--sklearn-color-unfitted-level-2);\n}\n\n#sk-container-id-1 div.sk-estimator.fitted input.sk-toggleable__control:checked~label.sk-toggleable__label {\n  /* fitted */\n  background-color: var(--sklearn-color-fitted-level-2);\n}\n\n#sk-container-id-1 div.sk-label label.sk-toggleable__label,\n#sk-container-id-1 div.sk-label label {\n  /* The background is the default theme color */\n  color: var(--sklearn-color-text-on-default-background);\n}\n\n/* On hover, darken the color of the background */\n#sk-container-id-1 div.sk-label:hover label.sk-toggleable__label {\n  color: var(--sklearn-color-text);\n  background-color: var(--sklearn-color-unfitted-level-2);\n}\n\n/* Label box, darken color on hover, fitted */\n#sk-container-id-1 div.sk-label.fitted:hover label.sk-toggleable__label.fitted {\n  color: var(--sklearn-color-text);\n  background-color: var(--sklearn-color-fitted-level-2);\n}\n\n/* Estimator label */\n\n#sk-container-id-1 div.sk-label label {\n  font-family: monospace;\n  font-weight: bold;\n  display: inline-block;\n  line-height: 1.2em;\n}\n\n#sk-container-id-1 div.sk-label-container {\n  text-align: center;\n}\n\n/* Estimator-specific */\n#sk-container-id-1 div.sk-estimator {\n  font-family: monospace;\n  border: 1px dotted var(--sklearn-color-border-box);\n  border-radius: 0.25em;\n  box-sizing: border-box;\n  margin-bottom: 0.5em;\n  /* unfitted */\n  background-color: var(--sklearn-color-unfitted-level-0);\n}\n\n#sk-container-id-1 div.sk-estimator.fitted {\n  /* fitted */\n  background-color: var(--sklearn-color-fitted-level-0);\n}\n\n/* on hover */\n#sk-container-id-1 div.sk-estimator:hover {\n  /* unfitted */\n  background-color: var(--sklearn-color-unfitted-level-2);\n}\n\n#sk-container-id-1 div.sk-estimator.fitted:hover {\n  /* fitted */\n  background-color: var(--sklearn-color-fitted-level-2);\n}\n\n/* Specification for estimator info (e.g. \"i\" and \"?\") */\n\n/* Common style for \"i\" and \"?\" */\n\n.sk-estimator-doc-link,\na:link.sk-estimator-doc-link,\na:visited.sk-estimator-doc-link {\n  float: right;\n  font-size: smaller;\n  line-height: 1em;\n  font-family: monospace;\n  background-color: var(--sklearn-color-background);\n  border-radius: 1em;\n  height: 1em;\n  width: 1em;\n  text-decoration: none !important;\n  margin-left: 1ex;\n  /* unfitted */\n  border: var(--sklearn-color-unfitted-level-1) 1pt solid;\n  color: var(--sklearn-color-unfitted-level-1);\n}\n\n.sk-estimator-doc-link.fitted,\na:link.sk-estimator-doc-link.fitted,\na:visited.sk-estimator-doc-link.fitted {\n  /* fitted */\n  border: var(--sklearn-color-fitted-level-1) 1pt solid;\n  color: var(--sklearn-color-fitted-level-1);\n}\n\n/* On hover */\ndiv.sk-estimator:hover .sk-estimator-doc-link:hover,\n.sk-estimator-doc-link:hover,\ndiv.sk-label-container:hover .sk-estimator-doc-link:hover,\n.sk-estimator-doc-link:hover {\n  /* unfitted */\n  background-color: var(--sklearn-color-unfitted-level-3);\n  color: var(--sklearn-color-background);\n  text-decoration: none;\n}\n\ndiv.sk-estimator.fitted:hover .sk-estimator-doc-link.fitted:hover,\n.sk-estimator-doc-link.fitted:hover,\ndiv.sk-label-container:hover .sk-estimator-doc-link.fitted:hover,\n.sk-estimator-doc-link.fitted:hover {\n  /* fitted */\n  background-color: var(--sklearn-color-fitted-level-3);\n  color: var(--sklearn-color-background);\n  text-decoration: none;\n}\n\n/* Span, style for the box shown on hovering the info icon */\n.sk-estimator-doc-link span {\n  display: none;\n  z-index: 9999;\n  position: relative;\n  font-weight: normal;\n  right: .2ex;\n  padding: .5ex;\n  margin: .5ex;\n  width: min-content;\n  min-width: 20ex;\n  max-width: 50ex;\n  color: var(--sklearn-color-text);\n  box-shadow: 2pt 2pt 4pt #999;\n  /* unfitted */\n  background: var(--sklearn-color-unfitted-level-0);\n  border: .5pt solid var(--sklearn-color-unfitted-level-3);\n}\n\n.sk-estimator-doc-link.fitted span {\n  /* fitted */\n  background: var(--sklearn-color-fitted-level-0);\n  border: var(--sklearn-color-fitted-level-3);\n}\n\n.sk-estimator-doc-link:hover span {\n  display: block;\n}\n\n/* \"?\"-specific style due to the `<a>` HTML tag */\n\n#sk-container-id-1 a.estimator_doc_link {\n  float: right;\n  font-size: 1rem;\n  line-height: 1em;\n  font-family: monospace;\n  background-color: var(--sklearn-color-background);\n  border-radius: 1rem;\n  height: 1rem;\n  width: 1rem;\n  text-decoration: none;\n  /* unfitted */\n  color: var(--sklearn-color-unfitted-level-1);\n  border: var(--sklearn-color-unfitted-level-1) 1pt solid;\n}\n\n#sk-container-id-1 a.estimator_doc_link.fitted {\n  /* fitted */\n  border: var(--sklearn-color-fitted-level-1) 1pt solid;\n  color: var(--sklearn-color-fitted-level-1);\n}\n\n/* On hover */\n#sk-container-id-1 a.estimator_doc_link:hover {\n  /* unfitted */\n  background-color: var(--sklearn-color-unfitted-level-3);\n  color: var(--sklearn-color-background);\n  text-decoration: none;\n}\n\n#sk-container-id-1 a.estimator_doc_link.fitted:hover {\n  /* fitted */\n  background-color: var(--sklearn-color-fitted-level-3);\n}\n</style><div id=\"sk-container-id-1\" class=\"sk-top-container\"><div class=\"sk-text-repr-fallback\"><pre>StandardScaler()</pre><b>In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook. <br />On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.</b></div><div class=\"sk-container\" hidden><div class=\"sk-item\"><div class=\"sk-estimator fitted sk-toggleable\"><input class=\"sk-toggleable__control sk-hidden--visually\" id=\"sk-estimator-id-1\" type=\"checkbox\" checked><label for=\"sk-estimator-id-1\" class=\"sk-toggleable__label fitted sk-toggleable__label-arrow fitted\">&nbsp;&nbsp;StandardScaler<a class=\"sk-estimator-doc-link fitted\" rel=\"noreferrer\" target=\"_blank\" href=\"https://scikit-learn.org/1.5/modules/generated/sklearn.preprocessing.StandardScaler.html\">?<span>Documentation for StandardScaler</span></a><span class=\"sk-estimator-doc-link fitted\">i<span>Fitted</span></span></label><div class=\"sk-toggleable__content fitted\"><pre>StandardScaler()</pre></div> </div></div></div></div>"
     },
     "execution_count": 4,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "from sklearn.preprocessing import StandardScaler\n",
    "from torch.utils.data import DataLoader\n",
    "\n",
    "\n",
    "scaler = StandardScaler()\n",
    "scaler.fit(x_train)"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 构建数据集"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2024-07-19T02:41:17.819730300Z",
     "start_time": "2024-07-19T02:41:17.784889Z"
    }
   },
   "outputs": [],
   "source": [
    "from torch.utils.data import Dataset\n",
    "\n",
    "class HousingDataset(Dataset):\n",
    "    def __init__(self, mode='train'):\n",
    "        self.x, self.y = dataset_maps[mode]\n",
    "        self.x = torch.from_numpy(scaler.transform(self.x)).float()\n",
    "        self.y = torch.from_numpy(self.y).float().reshape(-1, 1)\n",
    "            \n",
    "    def __len__(self):\n",
    "        return len(self.x)\n",
    "    \n",
    "    def __getitem__(self, idx):\n",
    "        return self.x[idx], self.y[idx]\n",
    "    \n",
    "    \n",
    "train_ds = HousingDataset(\"train\")\n",
    "valid_ds = HousingDataset(\"valid\")\n",
    "test_ds = HousingDataset(\"test\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "(tensor([-0.2981,  0.3523, -0.1092, -0.2506, -0.0341, -0.0060,  1.0806, -1.0611]),\n",
       " tensor([1.5140]))"
      ]
     },
     "execution_count": 7,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "train_ds[1]"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### DataLoader"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2024-07-19T02:42:17.381996400Z",
     "start_time": "2024-07-19T02:42:17.377001Z"
    }
   },
   "outputs": [],
   "source": [
    "from torch.utils.data import DataLoader\n",
    "\n",
    "\n",
    "batch_size = 8  #过大会导致GPU内存溢出，过小会导致训练时间过长\n",
    "train_loader = DataLoader(train_ds, batch_size=batch_size, shuffle=True)\n",
    "val_loader = DataLoader(valid_ds, batch_size=batch_size, shuffle=False)\n",
    "test_loader = DataLoader(test_ds, batch_size=batch_size, shuffle=False)"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 定义模型"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2024-07-19T02:59:27.312552700Z",
     "start_time": "2024-07-19T02:59:27.305582700Z"
    }
   },
   "outputs": [],
   "source": [
    "#回归模型我们只需要1个数\n",
    "\n",
    "class WideDeep(nn.Module):\n",
    "    def __init__(self, input_dim=8):\n",
    "        super().__init__()\n",
    "        self.deep = nn.Sequential(\n",
    "            nn.Linear(input_dim, 30), #30个神经元\n",
    "            nn.ReLU(),\n",
    "            nn.Linear(30, 30), #30个神经元\n",
    "            nn.ReLU()\n",
    "            )\n",
    "        # pytorch 需要自行计算输出输出维度\n",
    "        self.output_layer = nn.Linear(30 + input_dim, 1)\n",
    "        \n",
    "        # 初始化权重\n",
    "        self.init_weights()\n",
    "        \n",
    "    def init_weights(self):\n",
    "        \"\"\"使用 xavier 均匀分布来初始化全连接层的权重 W\"\"\"\n",
    "        for m in self.modules():\n",
    "            if isinstance(m, nn.Linear):\n",
    "                nn.init.xavier_uniform_(m.weight)\n",
    "                nn.init.zeros_(m.bias)\n",
    "        \n",
    "    def forward(self, x):\n",
    "        # x.shape [batch size, 8]\n",
    "        deep_output = self.deep(x)\n",
    "        # print(deep_output.shape)\n",
    "        # concat [batch size, 30] with x [batch size 8]，得到 [batch size, 38]\n",
    "        concat = torch.cat([x, deep_output], dim=1)\n",
    "        logits = self.output_layer(concat) # 输出层，输入维度是 38，输出维度是 1\n",
    "        # logits.shape [batch size, 1]\n",
    "        return logits"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "torch.Size([1, 8])\n"
     ]
    },
    {
     "data": {
      "text/plain": "torch.Size([1, 1])"
     },
     "execution_count": 16,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# train_ds[0][0]\n",
    "#验证模型是否正确\n",
    "input=train_ds[0][0].reshape(1, -1)\n",
    "print(input.shape)\n",
    "model=WideDeep()\n",
    "out=model(input)\n",
    "out.shape"
   ],
   "metadata": {
    "collapsed": false,
    "ExecuteTime": {
     "end_time": "2024-07-19T02:59:29.625632900Z",
     "start_time": "2024-07-19T02:59:29.616685600Z"
    }
   }
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2024-07-19T02:59:35.824277900Z",
     "start_time": "2024-07-19T02:59:35.821278900Z"
    }
   },
   "outputs": [],
   "source": [
    "class EarlyStopCallback:\n",
    "    def __init__(self, patience=5, min_delta=0.01):\n",
    "        \"\"\"\n",
    "\n",
    "        Args:\n",
    "            patience (int, optional): Number of epochs with no improvement after which training will be stopped.. Defaults to 5.\n",
    "            min_delta (float, optional): Minimum change in the monitored quantity to qualify as an improvement, i.e. an absolute \n",
    "                change of less than min_delta, will count as no improvement. Defaults to 0.01.\n",
    "        \"\"\"\n",
    "        self.patience = patience\n",
    "        self.min_delta = min_delta\n",
    "        self.best_metric = -1\n",
    "        self.counter = 0\n",
    "        \n",
    "    def __call__(self, metric):\n",
    "        if metric >= self.best_metric + self.min_delta:\n",
    "            # update best metric\n",
    "            self.best_metric = metric\n",
    "            # reset counter \n",
    "            self.counter = 0\n",
    "        else: \n",
    "            self.counter += 1\n",
    "            \n",
    "    @property\n",
    "    def early_stop(self):\n",
    "        return self.counter >= self.patience\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 18,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2024-07-19T02:59:38.251561200Z",
     "start_time": "2024-07-19T02:59:38.247567300Z"
    }
   },
   "outputs": [],
   "source": [
    "from sklearn.metrics import accuracy_score\n",
    "\n",
    "@torch.no_grad()\n",
    "def evaluating(model, dataloader, loss_fct):\n",
    "    loss_list = []\n",
    "    for datas, labels in dataloader:\n",
    "        datas = datas.to(device)\n",
    "        labels = labels.to(device)\n",
    "        # 前向计算\n",
    "        logits = model(datas)\n",
    "        loss = loss_fct(logits, labels)         # 验证集损失\n",
    "        loss_list.append(loss.item())\n",
    "        \n",
    "    return np.mean(loss_list)\n"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 训练"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 19,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2024-07-19T03:00:31.772121100Z",
     "start_time": "2024-07-19T02:59:52.886943100Z"
    }
   },
   "outputs": [
    {
     "data": {
      "text/plain": "  0%|          | 0/14520 [00:00<?, ?it/s]",
      "application/vnd.jupyter.widget-view+json": {
       "version_major": 2,
       "version_minor": 0,
       "model_id": "68ef26573bd14590a0d7168ad837bd3b"
      }
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "# 训练\n",
    "def training(\n",
    "    model, \n",
    "    train_loader, \n",
    "    val_loader, \n",
    "    epoch, \n",
    "    loss_fct, \n",
    "    optimizer, \n",
    "    tensorboard_callback=None,\n",
    "    save_ckpt_callback=None,\n",
    "    early_stop_callback=None,\n",
    "    eval_step=500,\n",
    "    ):\n",
    "    record_dict = {\n",
    "        \"train\": [],\n",
    "        \"val\": []\n",
    "    }\n",
    "    \n",
    "    global_step = 0\n",
    "    model.train()\n",
    "    with tqdm(total=epoch * len(train_loader)) as pbar:\n",
    "        for epoch_id in range(epoch):\n",
    "            # training\n",
    "            for datas, labels in train_loader:\n",
    "                datas = datas.to(device)\n",
    "                labels = labels.to(device)\n",
    "                # 梯度清空\n",
    "                optimizer.zero_grad()\n",
    "                # 模型前向计算\n",
    "                logits = model(datas)\n",
    "                # 计算损失\n",
    "                loss = loss_fct(logits, labels)\n",
    "                # 梯度回传\n",
    "                loss.backward()\n",
    "                # 调整优化器，包括学习率的变动等\n",
    "                optimizer.step()\n",
    " \n",
    "                loss = loss.cpu().item()\n",
    "                # record\n",
    "                \n",
    "                record_dict[\"train\"].append({\n",
    "                    \"loss\": loss, \"step\": global_step\n",
    "                })\n",
    "                \n",
    "                # evaluating\n",
    "                if global_step % eval_step == 0:\n",
    "                    model.eval()\n",
    "                    val_loss = evaluating(model, val_loader, loss_fct)\n",
    "                    record_dict[\"val\"].append({\n",
    "                        \"loss\": val_loss, \"step\": global_step\n",
    "                    })\n",
    "                    model.train()\n",
    "\n",
    "                    # 早停 Early Stop\n",
    "                    if early_stop_callback is not None:\n",
    "                        early_stop_callback(-val_loss)\n",
    "                        if early_stop_callback.early_stop:\n",
    "                            print(f\"Early stop at epoch {epoch_id} / global_step {global_step}\")\n",
    "                            return record_dict\n",
    "                    \n",
    "                # udate step\n",
    "                global_step += 1\n",
    "                pbar.update(1)\n",
    "                pbar.set_postfix({\"epoch\": epoch_id})\n",
    "        \n",
    "    return record_dict\n",
    "        \n",
    "\n",
    "epoch = 10\n",
    "\n",
    "model = WideDeep()\n",
    "\n",
    "# 1. 定义损失函数 采用交叉熵损失\n",
    "loss_fct = nn.MSELoss()\n",
    "# 2. 定义优化器 采用SGD\n",
    "# Optimizers specified in the torch.optim package\n",
    "optimizer = torch.optim.SGD(model.parameters(), lr=0.001, momentum=0.0)\n",
    "\n",
    "# 3. early stop\n",
    "early_stop_callback = EarlyStopCallback(patience=10, min_delta=1e-3)\n",
    "\n",
    "model = model.to(device)\n",
    "record = training(\n",
    "    model, \n",
    "    train_loader, \n",
    "    val_loader, \n",
    "    epoch, \n",
    "    loss_fct, \n",
    "    optimizer, \n",
    "    early_stop_callback=early_stop_callback,\n",
    "    eval_step=len(train_loader)\n",
    "    )"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 20,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2024-07-19T03:05:07.640519300Z",
     "start_time": "2024-07-19T03:05:07.153489500Z"
    }
   },
   "outputs": [
    {
     "data": {
      "text/plain": "<Figure size 640x480 with 1 Axes>",
      "image/png": "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gIyMjHrnVFdXY968eYiIiEBgYCBuvPFGFBQU1DsnJycHU6dOhb+/P6Kjo/HYY4/BbDbXO2f9+vUYPHgw9Ho9unXrhqVLl7q7ek578cUXIQgCHn74YccxX633mTNncNtttyEiIgJ+fn5ITU3Frl27HI9LkoSnnnoKsbGx8PPzw7hx43D8+PF61yguLsbs2bMRHByM0NBQ3H333SgvL693zv79+3HllVfCYDAgPj4eL7/8skfq1xiLxYInn3wSycnJ8PPzQ9euXfHss8/W23fAV+q9ceNGTJs2DXFxcRAEAd9//329xz1Zz2+++Qa9evWCwWBAamoqVq1aJXt97ZqrtyiKePzxx5GamoqAgADExcXhjjvuwNmzZ+tdw9fqfan7778fgiDg9ddfr3e8I9a7SZIP+PLLLyWdTid9/PHH0qFDh6R7771XCg0NlQoKCtq7aE6ZOHGitGTJEungwYNSenq6NGXKFCkhIUEqLy93nHP//fdL8fHx0po1a6Rdu3ZJl19+uTRy5EjH42azWerXr580btw4ae/evdKqVaukyMhI6YknnnCcc+rUKcnf319auHChdPjwYenNN9+U1Gq19PPPP3u0vo3ZsWOHlJSUJPXv319asGCB47gv1ru4uFhKTEyU5s6dK23fvl06deqU9Msvv0gnTpxwnPPiiy9KISEh0vfffy/t27dPuu6666Tk5GSpqqrKcc6kSZOkAQMGSNu2bZM2bdokdevWTZo5c6bj8dLSUqlTp07S7NmzpYMHD0rLly+X/Pz8pPfff9+j9bV77rnnpIiICOmnn36SMjMzpW+++UYKDAyU3njjDcc5vlLvVatWSX//+9+l7777TgIgrVixot7jnqrn5s2bJbVaLb388svS4cOHpX/84x+SVquVDhw44PF6l5SUSOPGjZO++uor6ejRo9LWrVulyy67TBoyZEi9a/havev67rvvpAEDBkhxcXHSv//973qPdcR6N8UngsVll10mzZs3z/G5xWKR4uLipBdeeKEdS9V6hYWFEgBpw4YNkiTZfiC1Wq30zTffOM45cuSIBEDaunWrJEm2b2yVSiXl5+c7znn33Xel4OBgyWQySZIkSX/5y1+kvn371nutW2+9VZo4caK7q9SssrIyqXv37lJaWpp01VVXOYKFr9b78ccfl6644oomH7darVJMTIz0yiuvOI6VlJRI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"
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "#画线要注意的是损失是不一定在零到1之间的\n",
    "def plot_learning_curves(record_dict, sample_step=500):\n",
    "    # build DataFrame\n",
    "    train_df = pd.DataFrame(record_dict[\"train\"]).set_index(\"step\").iloc[::sample_step]\n",
    "    val_df = pd.DataFrame(record_dict[\"val\"]).set_index(\"step\")\n",
    "\n",
    "    # plot\n",
    "    for idx, item in enumerate(train_df.columns):\n",
    "        plt.plot(train_df.index, train_df[item], label=f\"train_{item}\")\n",
    "        plt.plot(val_df.index, val_df[item], label=f\"val_{item}\")\n",
    "        plt.grid()\n",
    "        plt.legend()\n",
    "        # plt.xticks(range(0, train_df.index[-1], 10*sample_step), range(0, train_df.index[-1], 10*sample_step))\n",
    "        plt.xlabel(\"step\")\n",
    "\n",
    "        plt.show()\n",
    "\n",
    "plot_learning_curves(record, sample_step=500)  #横坐标是 steps"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 测试集"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 21,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2024-07-19T03:05:11.893942600Z",
     "start_time": "2024-07-19T03:05:11.750832200Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "loss:     0.4235\n"
     ]
    }
   ],
   "source": [
    "model.eval()\n",
    "loss = evaluating(model, val_loader, loss_fct)\n",
    "print(f\"loss:     {loss:.4f}\")"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "pytorch",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.10.8"
  },
  "orig_nbformat": 4
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
